/**
 * 
 */
package com.explgo.backend.service.impl;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.annotation.PostConstruct;

import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;

/**
 * This utility class looks up city name and its associated nileguide id.
 * 
 * @author Jialiang(Cain) Wang
 * 
 */
@Service
public class CityDictionary {
	/**
	 * The map from city name to nileguide id.
	 * */
	private Map<Character, Object> cityMap = new HashMap<>();

	public static final String CITY_DICTIONARY_DIR = "city_dict/";

	/**
	 * The max count of city entry in a list before refactor.
	 * */
	private static final int MAX_ENTRY_COUNT = 10;

	/**
	 * The map key for all entries under the leading characters.
	 * */
	private static final char KEY_ENTRY_LIST = '_';
	
	/**
	 * Gets the sorted city dictionary.
	 * */
	public Map<Character, Object> getCityMap() {
		return cityMap;
	}
	
	/**
	 * Gets the matched cities by leading string.
	 * */
	@SuppressWarnings("unchecked")
	public List<CityEntry> searchCity(String name) {
		List<CityEntry> result = new LinkedList<>();
		
		// debug options
		int comparisonCount = 0;
		int searchDepth = 0;
		
		Map<Character, Object> resultMap = cityMap;
		for (int i = 0; i < name.length(); i ++) {
			char c = name.charAt(i);
			c = Character.toLowerCase(c);
			
			Map<Character, Object> subMap = (Map<Character, Object>) resultMap.get(c);
			if (subMap == null) {
				List<CityEntry> entries = (List<CityEntry>) resultMap
						.get(KEY_ENTRY_LIST);
				
				// only a sub-set matches this name
				for (CityEntry entry : entries) {
					if (entry.getCityName().startsWith(name)) {
						comparisonCount ++;
						
						result.add(entry);
					}
				}
				
				searchDepth = i;
				break;
			} else {
				List<CityEntry> entries = (List<CityEntry>) subMap
						.get(KEY_ENTRY_LIST);
				if (i == name.length() - 1) {
					result.addAll(entries);
					searchDepth = i;
				}
				
				resultMap = subMap;
			}
		}
		
		System.out.println("Search depth: " + searchDepth);
		System.out.println("Comparison count: " + comparisonCount);
		
		return result;
	}

	/**
	 * Loads the city dictionary and puts them into a sorted map.
	 * */
	@PostConstruct
	public void initDictionary() {
		for (char c = 'A'; c <= 'Z'; c++) {
			Resource cityResource = new ClassPathResource(CITY_DICTIONARY_DIR
					+ c + ".txt");
			
			try (BufferedReader reader = new BufferedReader(
					new InputStreamReader(cityResource.getInputStream()));) {
				String line = null;
				while ((line = reader.readLine()) != null) {
					processCityDictionaryEntry(cityMap, line);
				}
			} catch (IOException e) {
				throw new RuntimeException(
						"Initializing city dictionary failed.", e);
			}
		}
	}
	
	/**
	 * Processes one city entry and put it in a sorted map.
	 * */
	private void processCityDictionaryEntry(Map<Character, Object> map,
			String line) {
		int dividerIndex = line.indexOf('=');
		if (dividerIndex > 0) {
			String cityName = line.substring(0, dividerIndex);
			String cityId = line.substring(dividerIndex + 1);

			CityEntry entry = new CityEntry();
			entry.setCityName(cityName);
			entry.setCityId(cityId);

			sortCityEntry(map, entry, 0);
		}
	}

	/**
	 * Puts the city entry into a sorted map.
	 * */
	@SuppressWarnings("unchecked")
	private void sortCityEntry(Map<Character, Object> parentMap,
			CityEntry entry, int level) {
		char leadingChar = entry.getCityName().charAt(level);
		leadingChar = Character.toLowerCase(leadingChar);

		Map<Character, Object> resultMap = (Map<Character, Object>) parentMap
				.get(leadingChar);
		if (resultMap == null) {
			resultMap = new HashMap<>();
			parentMap.put(leadingChar, resultMap);
		}

		List<CityEntry> entries = (List<CityEntry>) resultMap
				.get(KEY_ENTRY_LIST);
		if (entries == null) {
			entries = new LinkedList<>();
			resultMap.put(KEY_ENTRY_LIST, entries);
		}
		entries.add(entry);

		if (entries.size() == MAX_ENTRY_COUNT) {
			refactorMap(resultMap, level + 1);
		} else if (entries.size() > MAX_ENTRY_COUNT) {
			sortCityEntry(resultMap, entry, level + 1);
		}
	}

	/**
	 * Establishes sub-level hierarchies in the map.
	 * */
	@SuppressWarnings("unchecked")
	private void refactorMap(Map<Character, Object> map, int level) {
		List<CityEntry> entries = (List<CityEntry>) map.get(KEY_ENTRY_LIST);
		for (CityEntry entry : entries) {
			String cityName = entry.getCityName();

			if (cityName.length() > level) {
				putIntoSubMap(map, entry, level);
			}
		}
	}

	/**
	 * Puts the city entry into the sub-map mapped by the level-th character of
	 * city name.
	 * */
	@SuppressWarnings("unchecked")
	private void putIntoSubMap(Map<Character, Object> map, CityEntry entry,
			int level) {
		char keyChar = entry.getCityName().charAt(level);
		keyChar = Character.toLowerCase(keyChar);
		
		Map<Character, Object> subMap = (Map<Character, Object>) map
				.get(keyChar);
		if (subMap == null) {
			subMap = new HashMap<>();
			map.put(keyChar, subMap);
			
			List<CityEntry> subEntries = new LinkedList<>();
			subMap.put(KEY_ENTRY_LIST, subEntries);
		}

		List<CityEntry> subEntries = (List<CityEntry>) subMap
				.get(KEY_ENTRY_LIST);
		subEntries.add(entry);
	}
	
	/**
	 * A VO class that contains city name and id.
	 * */
	public static class CityEntry {
		/**
		 * The city name.
		 * */
		private String cityName;

		/**
		 * The city id.
		 * */
		private String cityId;

		/**
		 * @return the cityName
		 */
		public String getCityName() {
			return cityName;
		}

		/**
		 * @param cityName
		 *            the cityName to set
		 */
		public void setCityName(String cityName) {
			this.cityName = cityName;
		}

		/**
		 * @return the cityId
		 */
		public String getCityId() {
			return cityId;
		}

		/**
		 * @param cityId
		 *            the cityId to set
		 */
		public void setCityId(String cityId) {
			this.cityId = cityId;
		}
		
		public String toString() {
			return "(" + cityName + ": " + cityId + ")";
		}

	}
}

